대용량 계층적 삭제 배치 설계 패턴

NOTE

FK로 얽힌 여러 테이블에서 “읽으면서 동시에 삭제”해야 하는 대용량 배치의 설계 패턴 — Cursor Reader + CompositeItemWriter 계층 삭제 + Quartz/DB 기반 동적 스케줄링.


1. Reader — 왜 이 케이스는 Cursor가 필수인가

일반적으로는 커넥션 점유 시간이 짧은 Paging 방식이 안정적이라 기본으로 선호된다. 하지만 읽으면서 동시에 삭제하는 배치에서는 이야기가 다르다.

Paging 방식의 문제:
  페이지 1 조회 → 일부 삭제 → 페이지 2 조회
  → 삭제로 인해 뒤 데이터의 위치가 앞으로 밀림
  → 페이지 2 조회 시 이미 처리했어야 할 행을 건너뜀(SKIP) → 데이터 누락

Cursor는 최초 1회 조회한 ResultSet을 스트리밍으로 순회하므로, 순회 도중 삭제가 일어나도 이미 열려있는 스냅샷 기준으로 계속 처리되어 이런 “밀림에 의한 누락”이 생기지 않는다.

결론: 커넥션 점유가 다소 늘어나더라도, 읽는 도중 삭제/추가가 발생하는 배치는 Cursor 방식을 우선 고려해야 한다. (반대로 순수 조회·집계만 하는 배치라면 여전히 Paging이 안전한 기본값이다.)


2. Writer — 여러 테이블을 FK 순서대로 지우는 CompositeItemWriter

하나의 엔터티(회원 등)를 삭제하려면 하위 테이블부터 상위 테이블 순으로 여러 테이블을 지워야 FK 제약 위반이 나지 않는다.

CompositeItemWriter
  ├─ Writer 1: 하위 테이블 A 삭제 (자식)
  ├─ Writer 2: 하위 테이블 B 삭제 (자식)
  └─ Writer N: 메인 테이블 삭제 (부모, 항상 마지막)

핵심 규칙: Writer 리스트의 순서 자체가 삭제 순서다. 자식 테이블을 먼저, 메인(부모) 테이블을 리스트의 마지막에 둔다.

계층이 깊으면(예: 기기 → 회원사 → 그룹 → 상위그룹 → 규칙 → 사용자) Step을 계층 단계별로 나누고, Step 순서 자체도 “가장 하위 데이터부터 최상위 데이터까지” 배치한다.

안정성 옵션 — assertUpdates(false)

삭제 대상이 0건인 Writer가 있어도(그 회원이 특정 하위 테이블에 데이터가 없는 경우) 배치가 실패하지 않도록 assertUpdates(false)를 설정한다. 기본값(true)이면 “삭제된 행이 없음 = 오류”로 간주해 배치가 죽는다.


3. 스케줄러 — Quartz + DB 기반 동적 제어

배치 실행 주기를 코드 재배포 없이 바꾸려면, 스케줄 정보 자체를 DB 테이블로 관리하고 Quartz의 JDBC JobStore로 로드한다.

스케줄 관리 테이블(예: BATCH_JOB)
  ├─ JOB_ID           고유 식별자
  ├─ JOB_NM           배치 이름
  ├─ JOB_CLASS        실행할 Bean 이름
  ├─ CRON_EXPRESSION  실행 주기(cron)
  └─ USE_YN           활성화 여부
  • 스케줄러 클래스는 기동 시(또는 주기적으로) 이 테이블을 읽어 Quartz Trigger를 동적으로 생성한다.
  • 운영 이점: CRON_EXPRESSION이나 USE_YN을 DB에서 바꾸는 것만으로 서버 재시작 없이 배치 주기 변경/on-off가 가능하다.
  • 중복 실행 방지: @DisallowConcurrentExecution으로 동일 Job이 이전 실행이 끝나기 전에 다시 트리거되는 것을 막는다(직전 실행이 예상보다 오래 걸리는 경우의 안전장치).

4. 테스트 전략 — 격리 + 시나리오 기반

삭제 배치는 운영 데이터를 건드리는 만큼 테스트 설계가 특히 중요하다.

  • 데이터 격리: 테스트 전용 prefix(예: target%, TEST_XXX%)로 생성한 데이터만 대상으로 삼아, 반복 실행해도 실제 데이터에 영향이 없게 한다.
  • 시나리오 기반 데이터 준비: “삭제되어야 할 데이터”(예: 조건을 만족하는 오래된 데이터)와 “유지되어야 할 데이터”를 함께 만들어 넣고, 배치 실행 후 전자는 0건, 후자는 1건 이상 남아있는지로 정확성을 검증한다.

5. 신규 유사 배치를 만들 때 체크리스트

[ ] 이 배치가 "읽으면서 삭제/변경"하는가? → Cursor Reader 우선 검토
[ ] 삭제 대상 테이블이 FK로 여러 개 얽혀 있는가? → CompositeItemWriter, 자식→부모 순서 배치
[ ] Writer 중 일부가 0건 삭제될 수 있는가? → assertUpdates(false)
[ ] 실행 주기를 운영 중 자주 바꿔야 하는가? → Quartz + DB 동적 스케줄링 고려
[ ] 동일 Job이 겹쳐 실행될 위험이 있는가? → @DisallowConcurrentExecution
[ ] 테스트가 실제 데이터를 건드리지 않는가? → prefix 격리 + 삭제/유지 시나리오 동시 검증

🔗 관련